/**
 * Copyright (c) 2011 Nokia Corporation.
 *
 * CAUTION: This class is in very early experimental state.
 *
 */

#if !defined(NDEBUG) && defined(QT_CORE_LIB)
    #include <QDebug>
    #define LOG(x...) qDebug(x)
#else
    #define LOG(...) do {} while (0)
#endif

#include <math.h>
#include <memory.h>
#include <GLES2/gl2.h>
#include "GLES2SpriteBatch.h"

/*
   Shaders for the spritebatch rendering with a color multiply
*/
const char* strSpriteBatchFragmentShaderColorMultiplty =
    "uniform sampler2D sampler2d;\n"
    "varying lowp vec4 vertexColor;\n"
    "varying lowp vec2 texCoord;\n"
    "void main (void)\n"
    "{\n"
    "    gl_FragColor = texture2D(sampler2d, texCoord)*vertexColor;\n"
    "}";

const char* strSpriteBatchVertexShaderColorMultiply =
    "attribute highp vec3 vertex;\n"
    "uniform mediump mat4 projMatrix;\n"
    "uniform mediump mat4 im[16];\n"         // input
    "varying mediump vec2 texCoord;\n"
    "varying mediump vec4 vertexColor;\n"

    "void main(void)\n"
    "{\n"
        "mediump mat4 thisim = im[ int(vertex.z) ];\n"
        "highp vec2 transVertex = vec2( thisim[2][0], thisim[2][1] ) + mat2( thisim[0][0], thisim[0][1], thisim[1][0], thisim[1][1]) * (vertex.xy-vec2(thisim[2][2]-0.5, thisim[2][3]-0.5));\n"
        "gl_Position = vec4(transVertex,1,1) * projMatrix;\n"
        "vertexColor = vec4( thisim[3][0], thisim[3][1], thisim[3][2], thisim[3][3] );\n"
        "texCoord = (vertex.xy + vec2(0.5, 0.5)) * vec2(thisim[1][2], thisim[1][3]) + vec2(thisim[0][2], thisim[0][3]);\n"
    "}";


/*
   Shaders for the spritebatch rendering with pure texture
*/
const char* strSpriteBatchFragmentShaderPureTexture =
    "uniform sampler2D sampler2d;\n"
    "varying mediump vec2 texCoord;\n"
    "void main (void)\n"
    "{\n"
    "    gl_FragColor = texture2D(sampler2d, texCoord);\n"
    "}";

const char* strSpriteBatchVertexShaderPureTexture =
    "attribute highp vec3 vertex;\n"
    "uniform mediump mat4 projMatrix;\n"
    "uniform mediump mat4 im[16];\n"         // input
    "varying mediump vec2 texCoord;\n"
    "void main(void)\n"
    "{\n"
        "mediump mat4 thisim = im[ int(vertex.z) ];\n"
        "highp vec2 transVertex = vec2( thisim[2][0], thisim[2][1] ) + mat2( thisim[0][0], thisim[0][1], thisim[1][0], thisim[1][1]) * (vertex.xy-vec2(thisim[2][2]-0.5, thisim[2][3]-0.5));\n"
        "gl_Position = vec4(transVertex,1,1) * projMatrix;\n"
        "texCoord = (vertex.xy + vec2(0.5, 0.5)) * vec2(thisim[1][2], thisim[1][3]) + vec2(thisim[0][2], thisim[0][3]);\n"
    "}";



GLES2SpriteBatch::GLES2SpriteBatch(int w, int h ) : SpriteBatch(w,h)
{
    for (int f=0; f<COSINE_TABLE_SIZE; f++)
        cosineTable[f] = cosf( (float)f / (float)COSINE_TABLE_SIZE * 3.14159f * 2.0f );

    batchCounter = 0;


    // Initialize shaders
    GLint retval;

    for (int f=0; f<2; f++) {
        fragmentShader[f] = glCreateShader(GL_FRAGMENT_SHADER);

        if (f==0)
            glShaderSource(fragmentShader[f], 1, (const char**)&strSpriteBatchFragmentShaderColorMultiplty, NULL);
        else
            glShaderSource(fragmentShader[f], 1, (const char**)&strSpriteBatchFragmentShaderPureTexture, NULL);

        glCompileShader( fragmentShader[f] );
        glGetShaderiv(fragmentShader[f], GL_COMPILE_STATUS, &retval);
        if (!retval)
        {
            // failed
            LOG("failed to compile fshader: %d", f);
        } else {}

        vertexShader[f] = glCreateShader( GL_VERTEX_SHADER );
        if (f==0)
            glShaderSource( vertexShader[f], 1,(const char**)&strSpriteBatchVertexShaderColorMultiply, NULL );
        else
            glShaderSource( vertexShader[f], 1,(const char**)&strSpriteBatchVertexShaderPureTexture, NULL );

        glCompileShader( vertexShader[f] );
        glGetShaderiv( vertexShader[f], GL_COMPILE_STATUS, &retval );
        if (!retval) {
            // failed
            LOG("failed to compile vshader: %d", f);
        } else {}

        program[f] = glCreateProgram();
        glAttachShader(program[f], fragmentShader[f] );
        glAttachShader(program[f], vertexShader[f] );
            // Bind the custom vertex attributes
        glBindAttribLocation(program[f], 0, "vertex");

        glLinkProgram( program[f] );

            // Check if linking succeeded in the same way we checked for compilation success
        glGetProgramiv(program[f], GL_LINK_STATUS, &retval);
        if (!retval) {
            // failed
            LOG("failed to link program");
            GLint size = 0;
            glGetProgramiv(program[f], GL_INFO_LOG_LENGTH, &size);
            GLsizei realSize = size; // including the null terminator
            char * infoLog = new char[size];
            GLsizei obtainedLogSize = 0; // not including the null terminator
            glGetProgramInfoLog(program[f], realSize, &obtainedLogSize, infoLog);
            if (obtainedLogSize + 1 != realSize) {
                LOG("mismatch in sizes %d vs %d", realSize, obtainedLogSize);
            }
            LOG(infoLog);
            delete [] infoLog;
        } else {
            LOG("Finished building program %d", f );
        }


        samplerLocation[f] = glGetUniformLocation(program[f], "sampler2d");
        inputMatrixLocation[f] = glGetUniformLocation(program[f], "im");
        projmLocation[f] = glGetUniformLocation(program[f], "projMatrix");
    }


    glUseProgram( program[0] );
    glGenBuffers(1, &vbo);

    GLfloat vertices[] = {-0.5f,-0.5f,0.0f, 0.5f,-0.5f,0.0f, 0.5f,0.5f,0.0f,    // triangle 1
                          -0.5f,-0.5f,0.0f, 0.5f,0.5f,0.0f, -0.5f,0.5f,0.0f};   // triangle 2

    GLfloat *tempVertices = new GLfloat[ 3 * 6 * BATCH_SIZE];
    for (int f=0; f<BATCH_SIZE; f++) {
        memcpy( tempVertices+f*3*6, vertices, 3*6*sizeof(GLfloat ));
        for (int g=0; g<6; g++) tempVertices[ f*3*6+2+g*3 ] = f;            // mark the index for each triangle
    }

    glBindBuffer(GL_ARRAY_BUFFER, vbo );
    glBufferData(GL_ARRAY_BUFFER, BATCH_SIZE *6*sizeof(GLfloat)*3, tempVertices, GL_STATIC_DRAW );

    delete [] tempVertices;
}


GLES2SpriteBatch::~GLES2SpriteBatch()
{
    for (int f=0; f<2; f++) {
        if (program[f]) glDeleteProgram(program[f]);
        if (fragmentShader[f]) glDeleteShader(fragmentShader[f]);
        if (vertexShader[f]) glDeleteShader(vertexShader[f]);
    }


    if (vbo) glDeleteBuffers(1, &vbo );
    vbo = 0;
    for (int f=0; f<2; f++) {
        program[f] = 0;
        fragmentShader[f] = 0;
        vertexShader[f] = 0;
    }
}


void GLES2SpriteBatch::flushSprites()
{
    if (batchCounter<1) return;         // nothing to fill
    float angletointmul = (float)COSINE_TABLE_SIZE/3.14159f/2.0f;

    int selectedProgram = 0;
    if (allWhite==true) selectedProgram = 1;
    //if (selectedProgram==1) LOG("flushing with whiteprogram");
    if (currentProgram!=selectedProgram) setActiveProgram( selectedProgram );

        // Fill the inputTempMatrices
    float *m = inputMatrixTemp;
    SpriteDrawInfo *sdi = batchCollection;
    for (int f=0; f<batchCounter; f++)
    {
            // targetposition
        m[8+0] = sdi->posx;
        m[8+1] = sdi->posy;
            // origin
        m[8+2] = sdi->originX;
        m[8+3] = sdi->originY;

            // Destination orientation
        if (sdi->manualTransformActive)
        {
            m[0] = sdi->manualTransformMatrix[0][0];
            m[1] = sdi->manualTransformMatrix[0][1];
            m[4] = sdi->manualTransformMatrix[1][0];
            m[5] = sdi->manualTransformMatrix[1][1];
        }
        else
        {
            int iangle = ((int)( sdi->angle * angletointmul ) & COSINE_TABLE_AND);
            m[0] = cosineTable[iangle] * sdi->scaleX;
            m[1] = cosineTable[(iangle+COSINE_TABLE_SIZE/4)&COSINE_TABLE_AND] * sdi->scaleX;
            m[4+0] = cosineTable[(iangle+COSINE_TABLE_SIZE/4)&COSINE_TABLE_AND] * sdi->scaleY;
            m[4+1] = -cosineTable[iangle] * sdi->scaleY;
        }


            // Source rectangle
        m[2] = sdi->sourcex;
        m[3] = sdi->sourcey;
        m[4+2] = sdi->sourceWidth;
        m[4+3] = sdi->sourceHeight;

        m[12+0] = sdi->r;
        m[12+1] = sdi->g;
        m[12+2] = sdi->b;
        m[12+3] = sdi->a;

        m+=16;
        sdi++;
    }

    glUniformMatrix4fv(inputMatrixLocation[0], batchCounter,GL_FALSE, inputMatrixTemp );
    glDrawArrays(GL_TRIANGLES, 0, 6*batchCounter);

        // reset batch
    batchCounter = 0;
    allWhite = true;
}


#define WHITE_LIMIT 0.999f

void GLES2SpriteBatch::draw ( SpriteDrawInfo *sdi, int spriteCount )
{
    SpriteDrawInfo *i = sdi;
    while (spriteCount>0) {

        bool completelyWhite = false;
        if ( i->r>WHITE_LIMIT && i->g>WHITE_LIMIT && i->b>WHITE_LIMIT && i->a > WHITE_LIMIT ) completelyWhite = true;

        if (allWhite==true && completelyWhite==false && batchCounter>=2) {
            // current would break the "allWhite"-flag. Flush this batch before adding it.
            //LOG("Using the white program..");
            flushSprites();
        }

        if (i->textureHandle != currentTexture ) {
                // Texture have been changed, flush currently collected renderingdata
            flushSprites();
                // Change our current texture and activate it.
            currentTexture = i->textureHandle;
            glBindTexture( GL_TEXTURE_2D, currentTexture );
        }


        if (!completelyWhite) allWhite = false;
        memcpy( batchCollection + batchCounter, i, sizeof( SpriteDrawInfo ) );
        batchCounter++;
        if (batchCounter>=BATCH_SIZE) flushSprites();
        i++;
        spriteCount--;
    }
}

void GLES2SpriteBatch::setActiveProgram( int programIndex )
{
    glUseProgram( program[programIndex] );
    glUniform1i(samplerLocation[programIndex], 0);
    glUniformMatrix4fv(projmLocation[programIndex], 1, GL_FALSE, projectionMatrix );
    currentProgram = programIndex;
}

void GLES2SpriteBatch::begin( BlendMode bmode, TransformMode dTransform, float *customProjectionMatrix)
{
    if(batchCounter!=0)
    {
        LOG("Warning: SpriteBatch begin() called before end()");
    }

    currentDestinationTransform = dTransform;

    /*
    GLint viewport[4];
    glGetIntegerv(GL_VIEWPORT, viewport);
    currentViewportWidth = viewport[2];
    currentViewportHeight = viewport[3];
    */


    //glUseProgram( program[0] );
    //glUniform1i(samplerLocation[0], 0);

    if (customProjectionMatrix) {
        // set projection
        memcpy( projectionMatrix, customProjectionMatrix, 16 * sizeof( GLfloat ));
        //glUniformMatrix4fv(projmLocation[0], 1, GL_FALSE, customProjectionMatrix );
    } else {

        // Build a new projection matrix accoring screen's width and height
        GLfloat mproj[16] = {
            2.0f/(float)targetWidth, 0, 0, -1.0f,
            0, 2.0f/(float)targetHeight, 0, -1.0f,
            0, 0, 1, 0,
            0,0, 0, 1,
        };
        memcpy( projectionMatrix, mproj, 16 * sizeof( GLfloat ));

        //glUniformMatrix4fv(projmLocation[0], 1, GL_FALSE, mproj );
    }

    glDisable( GL_DEPTH_TEST );
    glDepthMask( GL_FALSE );
    glEnable( GL_BLEND );
    if (bmode==eALPHA)
        glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    else
        glBlendFunc( GL_SRC_ALPHA, GL_ONE);

    glEnableVertexAttribArray(0);
    glDisableVertexAttribArray(1);
    glDisableVertexAttribArray(2);
    glDisableVertexAttribArray(3);
    glDisable( GL_CULL_FACE );

    glBindBuffer(GL_ARRAY_BUFFER, vbo );
    glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,3*sizeof(GL_FLOAT),0);

    currentTexture = 0;
    allWhite = true;
    currentProgram = -1;
}

void GLES2SpriteBatch::end()
{
        // Draw everything that is collected.
    flushSprites();
}
